﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Security.Principal;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using Microsoft.ResourceManagement.Client;
using Microsoft.ResourceManagement.ObjectModel;

namespace Ensynch.FIM
{
    public class FimQuery
    {
        // Create a logger for use in this class.
        protected static readonly log4net.ILog log = log4net.LogManager.GetLogger(
                System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        #region Protected properties
        protected string _query;
        protected virtual string query
        {
            get
            {
                return _query;
            }
        }
        #endregion

        #region Public properties
        protected int? maxRows;
        public int? MaxRows
        {
            get
            {
                return maxRows;
            }
        }
        public string[] EnumerateAttributes
        {
            get
            {
                return attributes.EnumerateAttributes;
            }
        }
        public List<DataColumn> DataColumns
        {
            get
            {
                return attributes.DataColumns;
            }
        }
        protected FimQueryAttributeDictionary attributes;
        public IEnumerable<FimQueryAttribute> FimQueryAttributes
        {
            get
            {
                return attributes.Values;
            }
        }
        protected string objectType;
        public string ObjectType
        {
            get
            {
                if (objectType == null)
                {
                    objectType = getObjectType();
                }
                return objectType;
            }
        }
        /// <summary>
        /// Replace escaped percent signs ('\%').
        /// </summary>
        public string NormalizedXPath
        {
            get
            {
                string xpath = XPath;
                return xpath.Replace("\\%", "%");
            }
        }
        protected string rawXPath;
        public string XPath
        {
            get
            {
                string xpath = doParameterSubstitution(rawXPath);
                return xpath;
            }
        }
        protected bool whereConditionsParametersSubstituted = false;
        protected string rawWhereConditions;
        protected string whereConditions;
        public string WhereConditions
        {
            get
            {
                if (!whereConditionsParametersSubstituted)
                {
                    whereConditions = doParameterSubstitution(rawWhereConditions);
                    whereConditionsParametersSubstituted = true;
                }
                return whereConditions;
            }
        }
        protected Dictionary<string, bool> sortAttributes;
        public Dictionary<string, bool> SortAttributes
        {
            get
            {
                return sortAttributes;
            }
        }
        protected FimTools tools;
        public FimTools Tools
        {
            get
            {
                return tools;
            }
        }
        public string AttributeString
        {
            get
            {
                return attributes.ToString();
            }
        }
        public bool HasAttributes
        {
            get
            {
                return !attributes.IsEmpty;
            }
        }
        protected WindowsIdentity impersonatedUser;
        public WindowsIdentity ImpersonatedUser
        {
            get
            {
                return impersonatedUser;
            }
        }
        protected Dictionary<string, object> parameters;
        public Dictionary<string, object> Parameters
        {
            get 
            {
                if (parameters == null)
                {
                    parameters = new Dictionary<string, object>();
                }
                return parameters; 
            }
            set
            {
                FimTools.LogTrace(false);

                // For some reason, this doesn't work.
                //parameters = value;

                if (parameters == null)
                {
                    parameters = new Dictionary<string, object>();
                }
                parameters.Clear();
                foreach (KeyValuePair<string, object> item in value)
                {
                    parameters.Add(item.Key, item.Value);
                }

                #if TRACE
                foreach (KeyValuePair<string, object> item in parameters)
                {
                    log.DebugFormat("[set_Parameters] {0} = {1}", item.Key, item.Value);
                }
                #endif

                // Reset the calculated strings.
                whereConditionsParametersSubstituted = false;

                // And don't forget to propagate the new parameters down through the subqueries.
                attributes.SetParameters(parameters);
            }
        }
        #endregion

        #region Constructors

        public FimQuery(string query, DefaultClient client, WindowsIdentity impersonatedUser)
            : this(query, client)
        {
        }

        public FimQuery(string query, DefaultClient client)
            : this(client)
        {
            this._query = query;
            parseQueryFast();
        }

        protected FimQuery(DefaultClient client)
        {
            this.tools = new FimTools(client, false);
            attributes = new FimQueryAttributeDictionary(this);
        }

        public FimQuery(string query, FimTools tools)
            : this(tools)
        {
            this._query = query;
            parseQueryFast();
        }

        protected FimQuery(FimTools tools)
        {
            this.tools = tools;
            attributes = new FimQueryAttributeDictionary(this);
        }

        #endregion

        #region Protected methods

        protected void parseQueryFast()
        {
            DateTime start = DateTime.Now;
            DateTime finishParse;
            StringBuilder buffer;
            TimeSpan elapsed;

            StringBuilder token = new StringBuilder();
            int group = 0;
            string attributeString = null;

            int brackets = 0;
            int parens = 0;
            int quotes = 0;
            int singles = 0;

            for (int i = 0; i < query.Length; i++)
            {
                #region Ignore groups of outer matching characters.
                switch (query[i])
                {
                    case '[':
                    case ']':
                        if (parens == 0 && quotes == 0 && singles == 0)
                        {
                            brackets += query[i] == '[' ? 1 : -1;
                        }
                        break;
                    case '(':
                    case ')':
                        if (brackets == 0 && quotes == 0 && singles == 0)
                        {
                            parens += query[i] == '(' ? 1 : -1;
                        }
                        break;
                    case '"':
                        if (brackets == 0 && parens == 0 && singles == 0)
                        {
                            quotes = quotes == 0 ? 1 : 0;
                        }
                        break;
                    case '\'':
                        if (brackets == 0 && parens == 0 && quotes == 0)
                        {
                            singles = singles == 0 ? 1 : 0;
                        }
                        break;
                }
                if (brackets > 0 || parens > 0 || quotes > 0 || singles > 0)
                {
                    token.Append(query[i]);
                    continue;
                }
                #endregion

                string errMsg = null;

                #region Group 0: 'Select' keyword
                if (group == 0)
                {
                    int j;
                    if (findKeyword(i, out j, "select"))
                    {
                        i = j;
                        group++;
                        token = new StringBuilder();
                    }
                    else
                    {
                        // If no 'Select' keyword, skip the entire [Select [Top <int>] <attribute list> From]
                        // and go straight to the <xpath>.
                        group = 5;
                        token = new StringBuilder();
                        // Don't forget to manually "Select *".
                        attributes.SelectStar = true;
                    }
                }
                #endregion
                #region Group 1: 'Top' keyword
                if (group == 1)
                {
                    int j;
                    if (findKeyword(i, out j, "top"))
                    {
                        i = j;
                        group++;
                        token = new StringBuilder();
                    }
                    else
                    {
                        // If no 'Top' keyword, skip the entire [Top <int>]
                        // and go straight to the <attribute list>.
                        group = 3;
                        token = new StringBuilder();
                    }
                }
                #endregion
                #region Group 2: <int>
                if (group == 2) 
                {
                    switch (query[i])
                    {
                        case ' ':
                        case '\t':
                        case '\r':
                        case '\n':
                        case '\f':
                        case '\v':
                            if (token.Length == 0)
                            {
                                // Ignore leading whitespace.
                                continue;
                            }
                            else
                            {
                                // Now we've hit trailing whitespace. Time to parse.
                                int parsed;
                                if (!int.TryParse(token.ToString().Trim(), out parsed))
                                {
                                    throw new SyntaxErrorException(string.Format(
@"[parseQueryFast] Failed to parse integer in 'Top <int>' from: {2}
Usage: {0}
Input: {1}", sqlRegexUsage, query, token));
                                }
                                maxRows = parsed;
                                group++;
                                token = new StringBuilder();
                            }
                            break;
                        default:
                            break;
                    }
                }
                #endregion
                #region Group 3: Greedy match for <attribute list>
                if (group == 3)
                {
                    bool found = false;
                    int j;
                    switch (query[i])
                    {
                        case 'f':
                        case 'F':
                            if (findKeyword(i, out j, "from"))
                            {
                                i = j;
                                found = true;
                                // Since we've already found the 'From' keyword, just skip ahead to the <xpath>.
                                group = 5;
                            }
                            break;
                        default:
                            break;
                    }
                    if (found)
                    {
                        attributeString = token.ToString();
                        token = new StringBuilder();
                    }
                }
                #endregion
                #region Group 4: 'From' keyword
                if (group == 4)
                {
                    // We actually shouldn't even get here.
                    errMsg = "Internal error: unexpected group number: " + group;
                }
                #endregion
                #region Group 5: <xpath>
                if (group == 5)
                {
                    bool found = false;
                    int j;
                    switch (query[i])
                    {
                        case 'w':
                        case 'W':
                            if (findKeyword(i, out j, "where"))
                            {
                                i = j;
                                found = true;
                                // Since we've already found the 'Where' keyword, just skip ahead to the <conditions>.
                                group = 7;
                            }
                            break;
                        case 'o':
                        case 'O':
                            if (findKeywords(i, out j, "order", "by"))
                            {
                                i = j;
                                found = true;
                                // Since we've already found the 'Order by' keywords, just skip ahead to the <sort attribute>.
                                group = 9;
                            }
                            break;
                        default:
                            break;
                    }
                    if (found)
                    {
                        rawXPath = token.ToString().Trim();
                        token = new StringBuilder();
                    }
                }
                #endregion
                #region Group 6: 'Where' keyword
                if (group == 6)
                {
                    // We actually shouldn't even get here.
                    errMsg = "Internal error: unexpected group number: " + group;
                }
                #endregion
                #region Group 7: <conditions>
                if (group == 7)
                {
                    bool found = false;
                    int j;
                    switch (query[i])
                    {
                        case 'o':
                        case 'O':
                            if (findKeywords(i, out j, "order", "by"))
                            {
                                i = j;
                                found = true;
                                // Since we've already found the 'Order by' keywords, just skip ahead to the <sort attribute>.
                                group = 9;
                            }
                            break;
                        default:
                            break;
                    }
                    if (found)
                    {
                        rawWhereConditions = token.ToString().Trim();
                        token = new StringBuilder();
                    }
                }
                #endregion
                #region Group 8: 'Order by' keywords
                if (group == 8)
                {
                    // We actually shouldn't even get here.
                    errMsg = "Internal error: unexpected group number: " + group;
                }
                #endregion
                #region Group 9: <sort attribute>
                if (group == 9)
                {
                    bool found = false;
                    bool ascending = true;
                    int j;
                    switch (query[i])
                    {
                        case 'a':
                        case 'A':
                            if (findKeyword(i, out j, "asc"))
                            {
                                i = j;
                                found = true;
                                ascending = true;
                                // Since we've already found the 'Asc' keyword, we're done.
                            }
                            break;
                        case 'd':
                        case 'D':
                            if (findKeyword(i, out j, "desc"))
                            {
                                i = j;
                                found = true;
                                ascending = false;
                                // Since we've already found the 'Desc' keyword, we're done.
                            }
                            break;
                        default:
                            break;
                    }
                    if (found)
                    {
                        group = 11;
                        SetSortAttribute(token.ToString(), ascending);
                        token = new StringBuilder();
                        // These may be ending keywords, so skip to the next iteration.
                        continue;
                    }
                }
                #endregion
                #region Group 10: 'Asc' or 'Desc' keywords
                if (group == 10)
                {
                    // We actually shouldn't even get here.
                    errMsg = "Internal error: unexpected group number: " + group;
                }
                #endregion
                #region Group 11: Error
                if (group == 11)
                {
                    // We actually shouldn't even get here.
                    switch (query[i])
                    {
                        case ' ':
                        case '\t':
                        case '\r':
                        case '\n':
                        case '\f':
                        case '\v':
                            break;
                        default:
                            errMsg = "Internal error: unexpected group number: " + group;
                            break;
                    }
                }
                #endregion

                // Throw an exception if the error message isn't null.
                if (errMsg != null)
                {
                    int max = i + 10 >= query.Length ? query.Length - i : 10;
                    throw new SyntaxErrorException(string.Format(
@"[parseQueryFast] Syntax error at character {0}: ""{1}""
{2}
Usage: {3}
Input: {4}", i, query.Substring(i, max), errMsg, sqlRegexUsage, query));
                }

                // Finally, append the character to the token.
                token.Append(query[i]);
            }

            // We may have finished the loop without ending on a keyword.
            switch (group)
            {
                case 5:
                    rawXPath = token.ToString().Trim();
                    break;
                case 7:
                    rawWhereConditions = token.ToString().Trim();
                    break;
                case 9:
                    SetSortAttribute(token.ToString(), true);
                    break;
                default:
                    break;
            }

            if (attributeString != null)
            {
                parseAttributeString(attributeString);
            }

            parseParameters();

            finishParse = DateTime.Now;
            buffer = new StringBuilder();
            buffer.AppendLine("[parseQueryFast] Successfully completed.");
            elapsed = finishParse - start;
            buffer.AppendLine("Total elapsed time: " + elapsed.ToString());
            log.Debug(buffer.ToString());

            log.Debug("[parseQueryFast]\r\n" + this.ToString());
        }

        protected bool findKeywords(int i, out int j, params string[] keywords)
        {
            j = i;
            foreach (string keyword in keywords)
            {
                if (!findKeyword(j, out j, keyword))
                {
                    return false;
                }
            }
            return true;
        }

        protected bool findKeyword(int i, out int j, string keyword)
        {
            // First, look for the first non-whitespace character.
            for (j = i;  j < query.Length; j++)
            {
                switch (query[j])
                {
                    case ' ':
                    case '\t':
                    case '\r':
                    case '\n':
                    case '\f':
                    case '\v':
                        continue;
                        break;
                    default:
                        break;
                }
                break;
            }

            int len = keyword.Length;
            // Look for a leading word boundary first.
            if (j == 0)
            {
                // So far, so good.
            }
            else
            {
                switch (query[j - 1])
                {
                    case ' ':
                    case '\t':
                    case '\r':
                    case '\n':
                    case '\f':
                    case '\v':
                        // So far, so good.
                        break;
                    default:
                        return false;
                        break;
                }
            }

            if ((j + len) <= query.Length && query.Substring(j, len).ToLower() == keyword)
            {
                // So far, so good.
            }
            else
            {
                return false;
            }

            // Look for a trailing word boundary.
            if ((j+len) == query.Length)
            {
                // So far, so good.
            }
            else
            {
                switch (query[j + len])
                {
                    case ' ':
                    case '\t':
                    case '\r':
                    case '\n':
                    case '\f':
                    case '\v':
                        // So far, so good.
                        break;
                    default:
                        return false;
                        break;
                }
            }

            j += len;
            return true;
        }

        protected string doParameterSubstitution(string input)
        {
            FimTools.LogTrace(false);

            if (input == null)
            {
                #if TRACE
                log.Debug("[doParameterSubstitution] input == null");
                #endif
                return null;
            }

            if (Parameters.Count < 1)
            {
                #if TRACE
                log.Debug("[doParameterSubstitution] Parameters.Count < 1");
                #endif
                return input;
            }

            string output = input;
            StringBuilder msg = new StringBuilder("[doParameterSubstitution]\r\nInput: " + input);

            #region No need to do this with the right regex.
            //// First, substitute the escaped percent sign.
            //output = output.Replace("\\%", "&pct;");
            #endregion

            // Run the substitution on parameters (begin with %).
            foreach (KeyValuePair<string, object> item in Parameters)
            {
                msg.AppendFormat("\r\n{0} = {1}", item.Key, item.Value);
                output = output.Replace(item.Key, item.Value == null ? "" : item.Value.ToString());
            }

            msg.Append("\r\nOutput: " + output);
            log.Debug(msg.ToString());

            return output;
        }

        protected void parseParameters()
        {
            parseParameters(this.XPath);
            parseParameters(this.WhereConditions);
        }

        protected void parseParameters(string input)
        {
            if (input == null)
            {
                return;
            }

            List<string> paramNames = EnsynchTools.RegexCaptures(this.paramRegex, input);
            foreach (string param in paramNames)
            {
                if (!Parameters.ContainsKey(param))
                {
                    Parameters.Add(param, null);
                }
            }
        }

        protected void parseAttributeString(string attributeString)
        {
            DateTime start = DateTime.Now;
            DateTime finishParse;
            StringBuilder buffer;
            TimeSpan elapsed;

            if (attributeString.Trim() == "*")
            {
                attributes.SelectStar = true;
                return;
            }

            /// Here's a short list of supported syntax:            
            ///column
            ///column as name
            ///[column]
            ///[column] as [name]
            ///column.subattribute
            ///[column].[subattribute] as [name]

            string[] tokens;
            if (!EnsynchTools.TokenizeByMatchingChars(attributeString, '[', ']', out tokens))
            {
                throw new SyntaxErrorException("[parseAttributeString] Found unmatched brackets in attribute list: " + attributeString);
            }

            // First, encode the commas within brackets.
            StringBuilder encodedString = new StringBuilder();
            foreach (string token in tokens)
            {
                if (token.StartsWith("["))
                {
                    encodedString.Append(token.Replace(",", "&comma;"));
                }
                else
                {
                    encodedString.Append(token);
                }
            }

            // Now, split the string into groups with the Regex.
            MatchCollection matches = commaDelimitedAttributeRegex.Matches(encodedString.ToString());
            for (int i = 0; i < matches.Count; i++)
            {
                Match m = matches[i];
                GroupCollection groups = m.Groups;
                /// The regex always captures three groups.
                /// 1. The whole match.
                /// 2. The attribute name (no spaces).
                /// 3. The alias (may be blank).
                string attribute;
                string alias;
                if (groups.Count == 3)
                {
                    attribute = groups[1].Value.Trim();
                    alias = groups[2].Value.Trim();
                    if (attribute.IndexOf('.') >= 0)
                    {
                        tokens = attribute.Split('.');
                        attribute = tokens[0];
                        string subAttribute = tokens[1];
                        attributes[attribute].AddSubAttribute(subAttribute, alias.Replace("&comma;", ","));
                    }
                    else
                    {
                        attributes.Set(attribute, true, alias.Replace("&comma;", ","));
                    }
                }
                else
                {
                    throw new SyntaxErrorException("[parseAttributeString] Unexpected attribute syntax: " + m.ToString());
                }
            }

            finishParse = DateTime.Now;
            buffer = new StringBuilder();
            buffer.AppendLine("[parseAttributeString] Successfully completed.");
            elapsed = finishParse - start;
            buffer.AppendLine("Total elapsed time: " + elapsed.ToString());
            log.Debug(buffer.ToString());
        }

        /// <summary>
        /// Split an Xpath filter and determine the result object type.
        /// Note that if the filter has more than one slash(/)-delimited level,
        /// then this method will run a small query to get the ObjectType.
        /// </summary>
        /// <returns></returns>
        protected string getObjectType()
        {
            string objectType = null;

            /// We have to determine the complexity of the Xpath. For example:
            /// 1. If the Xpath has one level: 
            ///         /BindingDescription[BoundObjectType = /ObjectTypeDescription[Name = 'ManagementPolicyRule']]
            ///    Then the object type is trivial:
            ///         BindingDescription
            /// 2. If the Xpath has multiple levels:
            ///         /BindingDescription[BoundObjectType = /ObjectTypeDescription[Name = 'ManagementPolicyRule']]/BoundAttributeType
            ///    Then we have to run a query to find the object type:
            ///         SELECT TOP 1 ObjectType FROM <xpath>

            // Start by tokenizing the Xpath by groups within brackets.
            string[] groups;
            if (!EnsynchTools.TokenizeByMatchingChars(XPath, '[', ']', out groups))
            {
                throw new SyntaxErrorException("Failed to parse XPath; found unmatched brackets: " + XPath);
            }

            /// Copied from TokenizeByMatchingChars, here's what our groups look like: 
            /// Takes a string like "/Foo[Bar = /Blah[Bar = Foo]]/Blah[Foo = Bar]" 
            /// and creates groups of:
            ///     [0] = /Foo
            ///     [1] = [Bar = /Blah[Bar = Foo]]
            ///     [2] = /Blah
            ///     [3] = [Foo = Bar]

            int level = 0;
            string lastFound = null;
            for (int i = 0; i < groups.Length; i++)
            {
                string group = groups[i].Trim();
                if (!group.StartsWith("["))
                {
                    if (!group.StartsWith("/"))
                    {
                        throw new SyntaxErrorException("Failed to parse Xpath; expected leading '/'; found: " + group);
                    }
                    /// In the case of something like this: /ManagementPolicyRule/PrincipalSet
                    /// we have to further tokenize the string.
                    string[] tokens = group.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
                    foreach (string token in tokens)
                    {
                        level++;
                        lastFound = token;
                    }
                }
            }

            if (level < 1)
            {
                throw new SyntaxErrorException("Failed to parse Xpath; no levels with leading '/': " + XPath);
            }
            else if (level == 1 && lastFound.Trim() != "*")
            {
                objectType = lastFound.Trim();
            }
            else
            {
                DateTime start = DateTime.Now;
                DateTime finish;
                StringBuilder buffer;
                TimeSpan elapsed;

                try
                {
                    objectType = queryForObjectType(this.XPath, false);
                }
                catch (Exception exc)
                {
                    // This could happen if a predicate is parameterized.
                    log.Debug("[getObjectType] Query failed; retrying with stripped predicates.\r\n" + exc.Message);
                    objectType = queryForObjectType(this.XPath, true);
                }

                finish = DateTime.Now;
                buffer = new StringBuilder();
                buffer.AppendLine("[getObjectType] Successfully completed.");
                elapsed = finish - start;
                buffer.AppendLine("Total elapsed time: " + elapsed.ToString());
                log.Debug(buffer.ToString());
            }

            if (objectType != null)
            {
                log.Debug("[getObjectType] Successfully determined object type = " + objectType);
            }
            else
            {
                objectType = FimTools.OBJECT_RESOURCE;
                log.Debug("[getObjectType] Unable to determine object type from: " + this.XPath +
                    "\r\nAssigning object type to: " + objectType);
            }

            return objectType;
        }

        protected virtual string queryForObjectType(string xpath, bool stripPredicates)
        {
            string objectType = null;

            string newXPath = xpath;
            if (stripPredicates)
            {
                newXPath = this.stripPredicates(xpath);
            }

            // Unfortunately, we have to run a query to determine the object type.
            string top1 = string.Format("SELECT TOP 1 {0}, {1}, {2} FROM {3}",
                FimTools.ATTRIBUTE_DISPLAY_NAME,
                FimTools.ATTRIBUTE_OBJECT_ID,
                FimTools.ATTRIBUTE_OBJECT_TYPE,
                newXPath);
            FimQuery top1Query = new FimQuery(top1, this.Tools);
            IEnumerable<RmResource> objs = Tools.FimWsProxy.DefaultClient.Enumerate(
                top1Query.NormalizedXPath, top1Query.EnumerateAttributes, top1Query.MaxRows, top1Query.SortAttributes);

            if (objs != null)
            {
                List<RmResource> objList = objs.ToList();
                if (objList.Count > 0 && objList[0].ContainsKey(new RmAttributeName(FimTools.ATTRIBUTE_OBJECT_TYPE)))
                {
                    objectType = objList[0][FimTools.ATTRIBUTE_OBJECT_TYPE].ToString();
                }
            }

            return objectType;
        }

        protected string stripPredicates(string xpath)
        {
            if (xpath == null)
            {
                return null;
            }

            StringBuilder result = new StringBuilder();
            string[] tokens;
            EnsynchTools.TokenizeByMatchingChars(xpath, '[', ']', out tokens);
            foreach (string token in tokens)
            {
                if (token.IndexOf('[') < 0)
                {
                    result.Append(token);
                }
            }

            return result.ToString();
        }

        protected void logSuccessfulRegexCapture(string input, List<string> tokens)
        {
            StringBuilder buffer = new StringBuilder();
            buffer.AppendFormat(
@"[parseQuery] Successfully captured tokens from input:
{0}
", input);
            for (int i = 0; i < tokens.Count; i++)
            {
                buffer.AppendFormat("Tokens[{0}]: {1}\r\n", i, tokens[i]);
            }
            log.Debug(buffer.ToString());
        }

        protected void logFailedRegexCapture(string input)
        {
            log.DebugFormat(
@"[parseQuery] Failed to capture tokens from input:
{0}", input);
        }

        #endregion

        #region Public methods

        public void SetSortAttribute(string attribute, bool ascending)
        {
            if (attribute.IndexOf(',') >= 0)
            {
                throw new SyntaxErrorException(string.Format(
@"[parseQuery] Currently only supporting one sort column; found: {2}
Usage: {0}
Input: {1}", sqlRegexUsage, query, attribute));
            }

            // Since we're doing a 'Set', always create a new dictionary.
            sortAttributes = new Dictionary<string, bool>();

            if (sortAttributes.ContainsKey(attribute.Trim()))
            {
                throw new SyntaxErrorException(string.Format(
@"[parseQuery] Duplicate sort columns found: {2}
Usage: {0}
Input: {1}", sqlRegexUsage, query, attribute));
            }

            sortAttributes.Add(attribute.Trim(), ascending);
        }

        public FimQueryAttribute GetAttribute(string attribute)
        {
            if (!attributes.ContainsKey(attribute))
            {
                return null;
            }
            return attributes[attribute];
        }

        /// <summary>
        /// Intended for interactive use, as it will toggle attributes.SelectStar when necessary.
        /// </summary>
        public void AddAttribute(string attribute, string alias)
        {
            attributes.SelectStar = false;
            attributes.Set(attribute, true, alias);
        }

        /// <summary>
        /// Intended for interactive use, as it will toggle attributes.SelectStar when necessary.
        /// </summary>
        public void AddAttributes(Dictionary<string, string> additions)
        {
            attributes.SelectStar = false;
            foreach (var item in additions)
            {
                attributes.Set(item.Key, true, item.Value);
            }
        }

        /// <summary>
        /// Intended for interactive use, as it will toggle attributes.SelectStar when necessary.
        /// </summary>
        public void RemoveAttribute(string attribute, bool selectStarIfEmpty)
        {
            attributes[attribute].IncludeRaw = false;
            if (selectStarIfEmpty && attributes.IsEmpty)
            {
                attributes.SelectStar = true;
            }
        }

        /// <summary>
        /// Intended for interactive use, as it will toggle attributes.SelectStar when necessary.
        /// </summary>
        public void RemoveAttributes(Dictionary<string, string> subtractions, bool selectStarIfEmpty)
        {
            foreach (var item in subtractions)
            {
                attributes[item.Key].IncludeRaw = false;
            }
            if (selectStarIfEmpty && attributes.IsEmpty)
            {
                attributes.SelectStar = true;
            }
        }

        /// <summary>
        /// Toggles display names for ALL current attributes.
        /// Intended for interactive use.
        /// </summary>
        public void ToggleDisplayNames(bool showDisplayNames)
        {
            attributes.FlattenAttributeList();
            foreach (string attribute in attributes.Keys)
            {
                MaybeToggleDisplayName(attribute, showDisplayNames);
            }
        }

        /// <summary>
        /// Intended for interactive use.
        /// </summary>
        public void MaybeToggleDisplayNames(IEnumerable<string> attributeNames, bool showDisplayNames)
        {
            foreach (string attribute in attributeNames)
            {
                MaybeToggleDisplayName(attribute, showDisplayNames);
            }
        }

        /// <summary>
        /// Intended for interactive use.
        /// </summary>
        public void MaybeToggleDisplayName(string attributeName, bool showDisplayNames)
        {
            if (!attributes.ContainsKey(attributeName))
            {
                return;
            }

            FimQueryAttribute attribute = attributes[attributeName];

            if (attribute.AttributeTrimmed == FimTools.ATTRIBUTE_OBJECT_ID)
            {
                return;
            }

            if (attribute.RmAttributeType == RmFactory.RmAttributeType.Reference)
            {
                if (showDisplayNames)
                {
                    attribute.AddSubAttribute(FimTools.ATTRIBUTE_DISPLAY_NAME, attribute.AliasRaw);
                    attribute.IncludeRaw = false;
                }
                else
                {
                    FimQueryAttribute subAttribute = attribute.GetSubAttribute(FimTools.ATTRIBUTE_DISPLAY_NAME);
                    if (subAttribute != null)
                    {
                        attributes.Set(attribute.AttributeRaw, true, subAttribute.AliasRaw);
                        attribute.RemoveSubAttribute(FimTools.ATTRIBUTE_DISPLAY_NAME);
                    }
                }
            }
        }

        public FimQueryResult GetResult(Control context)
        {
            return new FimQueryResult(this, context);
        }

        public FimQueryResult GetResult()
        {
            return this.GetResult(null);
        }

        public override string ToString()
        {
            StringBuilder buffer = new StringBuilder();
            buffer.Append("SELECT ");
            if (maxRows != null)
            {
                buffer.Append("TOP " + maxRows);
            }
            buffer.AppendLine();
            buffer.AppendLine("\t" + attributes.ToString().Replace("\r\n", "\r\n\t"));
            buffer.AppendLine("FROM " + XPath);
            if (WhereConditions != null)
            {
                buffer.AppendLine("WHERE " + WhereConditions);
            }
            if (sortAttributes != null && sortAttributes.Count >0)
            {
                buffer.Append("ORDER BY ");
                bool first = true;
                foreach (string attribute in sortAttributes.Keys)
                {
                    if (first)
                    {
                        first = false;
                    }
                    else
                    {
                        buffer.Append("\r\n\t,");
                    }
                    buffer.Append(attribute);
                }
                buffer.Append(sortAttributes.Last().Value ? " ASC" : " DESC");
            }
            return buffer.ToString();
        }

        #endregion

        #region Regex

        Regex commaDelimitedAttributeRegex = new Regex(commaDelimitedAttributeRegexString_v2,
            RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
        const string commaDelimitedAttributeRegexString_v1 = @"(?:^|,)\s*([[]?[\.\w]+[\]]?)(?:\s+as\s+([[]?[^\],]*[\]]?))?";
        const string commaDelimitedAttributeRegexString_v2 = @"(?:^|,)\s*([^,\s]*)(?:\s+as\s+([[]?[^\],]*[\]]?))?";
        const string sqlRegexUsage = @"[Select [Top <int>] <attribute list> From] <xpath> [Where <conditions>] [Order by <sort attribute> [asc|desc]]";
        const string paramRegexString = @"(?:^|[^\\])(%\w+)";
        Regex paramRegex = new Regex(paramRegexString,
            RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);

        #endregion
        
    }
}
